function PriorityQueue(defaultPriority = 100) {
  let queue = [];

  function top() {
    if (queue.length > 0) {
      const raw = queue[queue.length - 1];
      return raw.value;
    } else {
      return void 0;
    }
  }

  function findIndex(array, fn) {
    let index = -1;
    for (let i = 0; i < array.length; i++) {
      if (fn(array[i])) {
        index = i;
        break;
      }
    }
    return index;
  }

  function add(value, priority = defaultPriority) {
    if (typeof priority !== 'number' || Number.isNaN(priority)) {
      throw new Error('Priority must be number');
    }

    const insertPos = findIndex(queue, t => t.priority > priority);
    if (insertPos === -1) {
      queue.push({ value, priority });
    } else {
      queue = [
        ...queue.slice(0, insertPos),
        { value, priority },
        ...queue.slice(0, insertPos)
      ];
    }
  }

  function pop() {
    if (queue.length === 0) {
      return void 0;
    } else {
      const raw = queue.pop();
      return raw.value;
    }
  }

  function forEach(func) {
    for (let i = queue.length; i > 0; i--) {
      const { value } = queue[i - 1];
      func(value, queue.length - i);
    }
  }

  function map(func) {
    let array = [];
    for (let i = queue.length; i > 0; i--) {
      const { value } = queue[i - 1];
      array.push(func(value, queue.length - i));
    }
    return array;
  }

  function filter(func) {
    let array = [];
    for (let i = queue.length; i > 0; i--) {
      const { value } = queue[i - 1];
      if (func(value, queue.length - i)) {
        array.push(value);
      }
    }
    return array;
  }

  function get(i) {
    if (
      typeof i !== 'number' ||
      Number.isNaN(i) ||
      i < 0 ||
      i >= queue.length
    ) {
      return void 0;
    }
    return queue[queue.length - i - 1];
  }

  // export interface
  const nope = Object.create(null);
  const interfaceObject = Object.defineProperties(nope, {
    top: { get: top },
    isEmpty: { get: () => queue.length === 0 },
    length: { get: () => queue.length },
    add: { value: add },
    pop: { value: pop },
    get: { value: get },
    forEach: { value: forEach },
    map: { value: map },
    filter: { value: filter }
  });
  Object.freeze(interfaceObject);

  return interfaceObject;
}

function Queue() {
  let queue = [];

  function top() {
    if (queue.length === 0) {
      return void 0;
    }
    return queue[queue.length - 1];
  }

  // export interface
  const interfaceObject = Object.defineProperties(queue, {
    top: { get: top, writable: false, configurable: false },
    isEmpty: { get: () => queue.length === 0, configurable: false }
  });

  return interfaceObject;
}

function LockFunction(asyncFunc) {
  const queue = [];
  let lock = false;

  async function exec(...rest) {
    if (lock) {
      return await new Promise((resolve, reject) =>
        queue.push({ resolve, reject })
      );
    }
    lock = true;
    try {
      const result = await asyncFunc.apply(this, rest);
      queue.forEach(({ resolve }) => resolve(result));
      return result;
    } catch (e) {
      queue.forEach(({ reject }) => reject(e));
      throw e;
    } finally {
      lock = false; // eslint-disable-line
    }
  }

  return exec;
}

function Cache(getStorageAsync, setStorageAsync) {
  if (!isFunc(getStorageAsync) || !isFunc(setStorageAsync)) {
    throw new Error('getStorageAsync and setStorageAsync must be function');
  }
  const cache = {};

  function isString(some) {
    return typeof some === 'string';
  }

  function isFunc(some) {
    return typeof some === 'function';
  }

  function makePromise(func) {
    return async function(...rest) {
      return await new Promise((resolve, reject) => {
        func(resolve, reject, ...rest);
      });
    };
  }

  function get(key) {
    if (!isString(key)) {
      throw new Error('cache key must be string');
    }
    return cache[key];
  }

  function set(key, data) {
    if (!isString(key)) {
      throw new Error('cache key must be string');
    }
    cache[key] = data;
  }

  const getAsync = makePromise((resolve, reject, key) => {
    if (!isString(key)) return reject('cache key must be string');
    getStorageAsync({ key })
      .then(({ data }) => {
        resolve(data);
      })
      .catch(() => resolve(void 0));
  });

  const setAsync = makePromise((resolve, reject, key, data) => {
    if (!isString(key)) return reject('cache key must be string');
    setStorageAsync({ key, data })
      .then(resolve)
      .catch(reject);
  });

  // export interface
  const interfaceObject = Object.create(null);
  Object.defineProperties(interfaceObject, {
    get: { value: get },
    set: { value: set },
    getAsync: { value: getAsync },
    setAsync: { value: setAsync }
  });
  return interfaceObject;
}

export { PriorityQueue, Queue, LockFunction, Cache };
